/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.avatica.util;

import java.sql.Time;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility functions for datetime types: date, time, timestamp.
 *
 * <p>Used by the JDBC driver.
 *
 * <p>TODO: review methods for performance. Due to allocations required, it may
 * be preferable to introduce a "formatter" with the required state.
 */
public class DateTimeUtils {
  /** The julian date of the epoch, 1970-01-01. */
  public static final int EPOCH_JULIAN = 2440588;

  private DateTimeUtils() {}

  //~ Static fields/initializers ---------------------------------------------

  /** The SimpleDateFormat string for ISO dates, "yyyy-MM-dd". */
  public static final String DATE_FORMAT_STRING = "yyyy-MM-dd";

  /** The SimpleDateFormat string for ISO times, "HH:mm:ss". */
  public static final String TIME_FORMAT_STRING = "HH:mm:ss";

  /** The SimpleDateFormat string for ISO timestamps, "yyyy-MM-dd HH:mm:ss". */
  public static final String TIMESTAMP_FORMAT_STRING =
      DATE_FORMAT_STRING + " " + TIME_FORMAT_STRING;

  /** Regex for date, YYYY-MM-DD. */
  private static final Pattern ISO_DATE_PATTERN =
      Pattern.compile("^(\\d{4})-([0]\\d|1[0-2])-([0-2]\\d|3[01])$");

  /** Regex for lenient date patterns. */
  private static final Pattern LENIENT_DATE_PATTERN =
      Pattern.compile("^\\s*(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s*$");

  /** Regex for time, HH:MM:SS. */
  private static final Pattern ISO_TIME_PATTERN =
      Pattern.compile("^([0-2]\\d):[0-5]\\d:[0-5]\\d(\\.\\d*)*$");

  /** The GMT time zone.
   *
   * @deprecated Use {@link #UTC_ZONE} */
  @Deprecated // to be removed before 2.0
  public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");

  /** The UTC time zone. */
  public static final TimeZone UTC_ZONE = TimeZone.getTimeZone("UTC");

  /** The Java default time zone. */
  public static final TimeZone DEFAULT_ZONE = TimeZone.getDefault();

  /**
   * The number of milliseconds in a second.
   */
  public static final long MILLIS_PER_SECOND = 1000L;

  /**
   * The number of milliseconds in a minute.
   */
  public static final long MILLIS_PER_MINUTE = 60000L;

  /**
   * The number of milliseconds in an hour.
   */
  public static final long MILLIS_PER_HOUR = 3600000L; // = 60 * 60 * 1000

  /**
   * The number of milliseconds in a day.
   *
   * <p>This is the modulo 'mask' used when converting
   * TIMESTAMP values to DATE and TIME values.
   */
  public static final long MILLIS_PER_DAY = 86400000; // = 24 * 60 * 60 * 1000

  /**
   * The number of seconds in a day.
   */
  public static final long SECONDS_PER_DAY = 86_400; // = 24 * 60 * 60

  /**
   * The number of nanoseconds in a millisecond.
   */
  public static final long NANOS_PER_MILLI = 1000000L;

  /**
   * Calendar set to the epoch (1970-01-01 00:00:00 UTC). Useful for
   * initializing other values. Calendars are not immutable, so be careful not
   * to screw up this object for everyone else.
   */
  public static final Calendar ZERO_CALENDAR;

  private static final OffsetDateTimeHandler OFFSET_DATE_TIME_HANDLER;

  static {
    ZERO_CALENDAR = Calendar.getInstance(DateTimeUtils.UTC_ZONE, Locale.ROOT);
    ZERO_CALENDAR.setTimeInMillis(0);
    OffsetDateTimeHandler h;
    try {
      h = new ReflectiveOffsetDateTimeHandler();
    } catch (ClassNotFoundException e) {
      h = new NoopOffsetDateTimeHandler();
    }
    OFFSET_DATE_TIME_HANDLER = h;
  }

  //~ Methods ----------------------------------------------------------------

  /**
   * Parses a string using {@link SimpleDateFormat} and a given pattern. This
   * method parses a string at the specified parse position and if successful,
   * updates the parse position to the index after the last character used.
   * The parsing is strict and requires months to be less than 12, days to be
   * less than 31, etc.
   *
   * @param s       string to be parsed
   * @param dateFormat Date format
   * @param tz      time zone in which to interpret string. Defaults to the Java
   *                default time zone
   * @param pp      position to start parsing from
   * @return a Calendar initialized with the parsed value, or null if parsing
   * failed. If returned, the Calendar is configured to the GMT time zone.
   */
  private static Calendar parseDateFormat(String s, DateFormat dateFormat,
      TimeZone tz, ParsePosition pp) {
    if (tz == null) {
      tz = DEFAULT_ZONE;
    }
    Calendar ret = Calendar.getInstance(tz, Locale.ROOT);
    dateFormat.setCalendar(ret);
    dateFormat.setLenient(false);

    final Date d = dateFormat.parse(s, pp);
    if (null == d) {
      return null;
    }
    ret.setTime(d);
    ret.setTimeZone(UTC_ZONE);
    return ret;
  }

  @Deprecated // to be removed before 2.0
  public static Calendar parseDateFormat(String s, String pattern,
      TimeZone tz) {
    return parseDateFormat(s, new SimpleDateFormat(pattern, Locale.ROOT), tz);
  }

  /**
   * Parses a string using {@link SimpleDateFormat} and a given pattern. The
   * entire string must match the pattern specified.
   *
   * @param s       string to be parsed
   * @param dateFormat Date format
   * @param tz      time zone in which to interpret string. Defaults to the Java
   *                default time zone
   * @return a Calendar initialized with the parsed value, or null if parsing
   * failed. If returned, the Calendar is configured to the UTC time zone.
   */
  public static Calendar parseDateFormat(String s, DateFormat dateFormat,
      TimeZone tz) {
    ParsePosition pp = new ParsePosition(0);
    Calendar ret = parseDateFormat(s, dateFormat, tz, pp);
    if (pp.getIndex() != s.length()) {
      // Didn't consume entire string - not good
      return null;
    }
    return ret;
  }

  @Deprecated // to be removed before 2.0
  public static PrecisionTime parsePrecisionDateTimeLiteral(
      String s,
      String pattern,
      TimeZone tz) {
    assert pattern != null;
    return parsePrecisionDateTimeLiteral(s,
        new SimpleDateFormat(pattern, Locale.ROOT), tz, 3);
  }

  /**
   * Parses a string using {@link SimpleDateFormat} and a given pattern, and
   * if present, parses a fractional seconds component. The fractional seconds
   * component must begin with a decimal point ('.') followed by numeric
   * digits. The precision is rounded to a maximum of 3 digits of fractional
   * seconds precision (to obtain milliseconds).
   *
   * @param s       string to be parsed
   * @param dateFormat Date format
   * @param tz      time zone in which to interpret string. Defaults to the
   *                local time zone
   * @return a {@link DateTimeUtils.PrecisionTime PrecisionTime} initialized
   * with the parsed value, or null if parsing failed. The PrecisionTime
   * contains a GMT Calendar and a precision.
   */
  public static PrecisionTime parsePrecisionDateTimeLiteral(String s,
      DateFormat dateFormat, TimeZone tz, int maxPrecision) {
    final ParsePosition pp = new ParsePosition(0);
    final Calendar cal = parseDateFormat(s, dateFormat, tz, pp);
    if (cal == null) {
      return null; // Invalid date/time format
    }

    // Note: the Java SimpleDateFormat 'S' treats any number after
    // the decimal as milliseconds. That means 12:00:00.9 has 9
    // milliseconds and 12:00:00.9999 has 9999 milliseconds.
    int p = 0;
    String secFraction = "";
    if (pp.getIndex() < s.length()) {
      // Check to see if rest is decimal portion
      if (s.charAt(pp.getIndex()) != '.') {
        return null;
      }

      // Skip decimal sign
      pp.setIndex(pp.getIndex() + 1);

      // Parse decimal portion
      if (pp.getIndex() < s.length()) {
        secFraction = s.substring(pp.getIndex());
        if (!secFraction.matches("\\d+")) {
          return null;
        }
        NumberFormat nf = NumberFormat.getIntegerInstance(Locale.ROOT);
        Number num = nf.parse(s, pp);
        if (num == null || pp.getIndex() != s.length()) {
          // Invalid decimal portion
          return null;
        }

        // Determine precision - only support prec 3 or lower
        // (milliseconds) Higher precisions are quietly rounded away
        p = secFraction.length();
        if (maxPrecision >= 0) {
          // If there is a maximum precision, ignore subsequent digits
          p = Math.min(maxPrecision, p);
          secFraction = secFraction.substring(0, p);
        }

        // Calculate milliseconds
        String millis = secFraction;
        if (millis.length() > 3) {
          millis = secFraction.substring(0, 3);
        }
        while (millis.length() < 3) {
          millis = millis + "0";
        }

        int ms = Integer.parseInt(millis);
        cal.add(Calendar.MILLISECOND, ms);
      }
    }

    assert pp.getIndex() == s.length();
    return new PrecisionTime(cal, secFraction, p);
  }

  /**
   * Gets the active time zone based on a Calendar argument
   */
  public static TimeZone getTimeZone(Calendar cal) {
    if (cal == null) {
      return DEFAULT_ZONE;
    }
    return cal.getTimeZone();
  }

  /**
   * Checks if the date/time format is valid
   *
   * @param pattern {@link SimpleDateFormat}  pattern
   * @throws IllegalArgumentException if the given pattern is invalid
   */
  public static void checkDateFormat(String pattern) {
    new SimpleDateFormat(pattern, Locale.ROOT);
  }

  /**
   * Creates a new date formatter with Farrago specific options. Farrago
   * parsing is strict and does not allow values such as day 0, month 13, etc.
   *
   * @param format {@link SimpleDateFormat}  pattern
   */
  public static SimpleDateFormat newDateFormat(String format) {
    SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.ROOT);
    sdf.setLenient(false);
    return sdf;
  }

  /** Helper for CAST({timestamp} AS VARCHAR(n)). */
  public static String unixTimestampToString(long timestamp) {
    return unixTimestampToString(timestamp, 0);
  }

  public static String unixTimestampToString(long timestamp, int precision) {
    final StringBuilder buf = new StringBuilder(17);
    int date = (int) (timestamp / MILLIS_PER_DAY);
    int time = (int) (timestamp % MILLIS_PER_DAY);
    if (time < 0) {
      --date;
      time += MILLIS_PER_DAY;
    }
    unixDateToString(buf, date);
    buf.append(' ');
    unixTimeToString(buf, time, precision);
    return buf.toString();
  }

  /** Helper for CAST({timestamp} AS VARCHAR(n)). */
  public static String unixTimeToString(int time) {
    return unixTimeToString(time, 0);
  }

  public static String unixTimeToString(int time, int precision) {
    final StringBuilder buf = new StringBuilder(8);
    unixTimeToString(buf, time, precision);
    return buf.toString();
  }

  private static void unixTimeToString(StringBuilder buf, int time,
      int precision) {
    int h = time / 3600000;
    int time2 = time % 3600000;
    int m = time2 / 60000;
    int time3 = time2 % 60000;
    int s = time3 / 1000;
    int ms = time3 % 1000;
    int2(buf, h);
    buf.append(':');
    int2(buf, m);
    buf.append(':');
    int2(buf, s);
    if (precision > 0) {
      buf.append('.');
      while (precision > 0) {
        buf.append((char) ('0' + (ms / 100)));
        ms = ms % 100;
        ms = ms * 10;
        --precision;
      }
    }
  }

  private static void int2(StringBuilder buf, int i) {
    buf.append((char) ('0' + (i / 10) % 10));
    buf.append((char) ('0' + i % 10));
  }

  private static void int4(StringBuilder buf, int i) {
    buf.append((char) ('0' + (i / 1000) % 10));
    buf.append((char) ('0' + (i / 100) % 10));
    buf.append((char) ('0' + (i / 10) % 10));
    buf.append((char) ('0' + i % 10));
  }

  /** Helper for CAST({date} AS VARCHAR(n)). */
  public static String unixDateToString(int date) {
    final StringBuilder buf = new StringBuilder(10);
    unixDateToString(buf, date);
    return buf.toString();
  }

  private static void unixDateToString(StringBuilder buf, int date) {
    julianToString(buf, date + EPOCH_JULIAN);
  }

  private static void julianToString(StringBuilder buf, int julian) {
    // this shifts the epoch back to astronomical year -4800 instead of the
    // start of the Christian era in year AD 1 of the proleptic Gregorian
    // calendar.
    int j = julian + 32044;
    int g = j / 146097;
    int dg = j % 146097;
    int c = (dg / 36524 + 1) * 3 / 4;
    int dc = dg - c * 36524;
    int b = dc / 1461;
    int db = dc % 1461;
    int a = (db / 365 + 1) * 3 / 4;
    int da = db - a * 365;

    // integer number of full years elapsed since March 1, 4801 BC
    int y = g * 400 + c * 100 + b * 4 + a;
    // integer number of full months elapsed since the last March 1
    int m = (da * 5 + 308) / 153 - 2;
    // number of days elapsed since day 1 of the month
    int d = da - (m + 4) * 153 / 5 + 122;
    int year = y - 4800 + (m + 2) / 12;
    int month = (m + 2) % 12 + 1;
    int day = d + 1;
    int4(buf, year);
    buf.append('-');
    int2(buf, month);
    buf.append('-');
    int2(buf, day);
  }

  public static String intervalYearMonthToString(int v, TimeUnitRange range) {
    final StringBuilder buf = new StringBuilder();
    if (v >= 0) {
      buf.append('+');
    } else {
      buf.append('-');
      v = -v;
    }
    final int y;
    final int m;
    switch (range) {
    case YEAR:
      v = roundUp(v, 12);
      y = v / 12;
      buf.append(y);
      break;
    case YEAR_TO_MONTH:
      y = v / 12;
      buf.append(y);
      buf.append('-');
      m = v % 12;
      number(buf, m, 2);
      break;
    case MONTH:
      m = v;
      buf.append(m);
      break;
    default:
      throw new AssertionError(range);
    }
    return buf.toString();
  }

  public static StringBuilder number(StringBuilder buf, int v, int n) {
    for (int k = digitCount(v); k < n; k++) {
      buf.append('0');
    }
    return buf.append(v);
  }

  public static int digitCount(int v) {
    for (int n = 1;; n++) {
      v /= 10;
      if (v == 0) {
        return n;
      }
    }
  }

  private static int roundUp(int dividend, int divisor) {
    int remainder = dividend % divisor;
    dividend -= remainder;
    if (remainder * 2 > divisor) {
      dividend += divisor;
    }
    return dividend;
  }

  /** Cheap, unsafe, long power. power(2, 3) returns 8. */
  public static long powerX(long a, long b) {
    long x = 1;
    while (b > 0) {
      x *= a;
      --b;
    }
    return x;
  }

  public static String intervalDayTimeToString(long v, TimeUnitRange range,
      int scale) {
    final StringBuilder buf = new StringBuilder();
    if (v >= 0) {
      buf.append('+');
    } else {
      buf.append('-');
      v = -v;
    }
    final long ms;
    final long s;
    final long m;
    final long h;
    final long d;
    switch (range) {
    case DAY_TO_SECOND:
      v = roundUp(v, powerX(10, 3 - scale));
      ms = v % 1000;
      v /= 1000;
      s = v % 60;
      v /= 60;
      m = v % 60;
      v /= 60;
      h = v % 24;
      v /= 24;
      d = v;
      buf.append((int) d);
      buf.append(' ');
      number(buf, (int) h, 2);
      buf.append(':');
      number(buf, (int) m, 2);
      buf.append(':');
      number(buf, (int) s, 2);
      fraction(buf, scale, ms);
      break;
    case DAY_TO_MINUTE:
      v = roundUp(v, 1000 * 60);
      v /= 1000;
      v /= 60;
      m = v % 60;
      v /= 60;
      h = v % 24;
      v /= 24;
      d = v;
      buf.append((int) d);
      buf.append(' ');
      number(buf, (int) h, 2);
      buf.append(':');
      number(buf, (int) m, 2);
      break;
    case DAY_TO_HOUR:
      v = roundUp(v, 1000 * 60 * 60);
      v /= 1000;
      v /= 60;
      v /= 60;
      h = v % 24;
      v /= 24;
      d = v;
      buf.append((int) d);
      buf.append(' ');
      number(buf, (int) h, 2);
      break;
    case DAY:
      v = roundUp(v, 1000 * 60 * 60 * 24);
      d = v / (1000 * 60 * 60 * 24);
      buf.append((int) d);
      break;
    case HOUR:
      v = roundUp(v, 1000 * 60 * 60);
      v /= 1000;
      v /= 60;
      v /= 60;
      h = v;
      buf.append((int) h);
      break;
    case HOUR_TO_MINUTE:
      v = roundUp(v, 1000 * 60);
      v /= 1000;
      v /= 60;
      m = v % 60;
      v /= 60;
      h = v;
      buf.append((int) h);
      buf.append(':');
      number(buf, (int) m, 2);
      break;
    case HOUR_TO_SECOND:
      v = roundUp(v, powerX(10, 3 - scale));
      ms = v % 1000;
      v /= 1000;
      s = v % 60;
      v /= 60;
      m = v % 60;
      v /= 60;
      h = v;
      buf.append((int) h);
      buf.append(':');
      number(buf, (int) m, 2);
      buf.append(':');
      number(buf, (int) s, 2);
      fraction(buf, scale, ms);
      break;
    case MINUTE_TO_SECOND:
      v = roundUp(v, powerX(10, 3 - scale));
      ms = v % 1000;
      v /= 1000;
      s = v % 60;
      v /= 60;
      m = v;
      buf.append((int) m);
      buf.append(':');
      number(buf, (int) s, 2);
      fraction(buf, scale, ms);
      break;
    case MINUTE:
      v = roundUp(v, 1000 * 60);
      v /= 1000;
      v /= 60;
      m = v;
      buf.append((int) m);
      break;
    case SECOND:
      v = roundUp(v, powerX(10, 3 - scale));
      ms = v % 1000;
      v /= 1000;
      s = v;
      buf.append((int) s);
      fraction(buf, scale, ms);
      break;
    default:
      throw new AssertionError(range);
    }
    return buf.toString();
  }

  /**
   * Rounds a dividend to the nearest divisor.
   * For example roundUp(31, 10) yields 30; roundUp(37, 10) yields 40.
   * @param dividend Number to be divided
   * @param divisor Number to divide by
   * @return Rounded dividend
   */
  private static long roundUp(long dividend, long divisor) {
    long remainder = dividend % divisor;
    dividend -= remainder;
    if (remainder * 2 > divisor) {
      dividend += divisor;
    }
    return dividend;
  }

  private static void fraction(StringBuilder buf, int scale, long ms) {
    if (scale > 0) {
      buf.append('.');
      long v1 = scale == 3 ? ms
          : scale == 2 ? ms / 10
          : scale == 1 ? ms / 100
            : 0;
      number(buf, (int) v1, scale);
    }
  }

  public static int dateStringToUnixDate(String s) {
    validateLenientDate(s);
    int hyphen1 = s.indexOf('-');
    int y;
    int m;
    int d;
    if (hyphen1 < 0) {
      y = Integer.parseInt(s.trim());
      m = 1;
      d = 1;
    } else {
      y = Integer.parseInt(s.substring(0, hyphen1).trim());
      final int hyphen2 = s.indexOf('-', hyphen1 + 1);
      if (hyphen2 < 0) {
        m = Integer.parseInt(s.substring(hyphen1 + 1).trim());
        d = 1;
      } else {
        m = Integer.parseInt(s.substring(hyphen1 + 1, hyphen2).trim());
        d = Integer.parseInt(s.substring(hyphen2 + 1).trim());
      }
    }
    return ymdToUnixDate(y, m, d);
  }

  public static int timeStringToUnixDate(String v) {
    return timeStringToUnixDate(v, 0);
  }

  public static int timeStringToUnixDate(String v, int start) {
    final int colon1 = v.indexOf(':', start);
    int hour;
    int minute;
    int second;
    int milli;
    if (colon1 < 0) {
      hour = Integer.parseInt(v.trim());
      minute = 0;
      second = 0;
      milli = 0;
    } else {
      hour = Integer.parseInt(v.substring(start, colon1).trim());
      final int colon2 = v.indexOf(':', colon1 + 1);
      if (colon2 < 0) {
        minute = Integer.parseInt(v.substring(colon1 + 1).trim());
        second = 0;
        milli = 0;
      } else {
        minute = Integer.parseInt(v.substring(colon1 + 1, colon2).trim());
        int dot = v.indexOf('.', colon2);
        if (dot < 0) {
          second = Integer.parseInt(v.substring(colon2 + 1).trim());
          milli = 0;
        } else {
          second = Integer.parseInt(v.substring(colon2 + 1, dot).trim());
          milli = parseFraction(v.substring(dot + 1).trim(), 100);
        }
      }
    }
    return hour * (int) MILLIS_PER_HOUR
        + minute * (int) MILLIS_PER_MINUTE
        + second * (int) MILLIS_PER_SECOND
        + milli;
  }

  /** Parses a fraction, multiplying the first character by {@code multiplier},
   * the second character by {@code multiplier / 10},
   * the third character by {@code multiplier / 100}, and so forth.
   *
   * <p>For example, {@code parseFraction("1234", 100)} yields {@code 123}. */
  private static int parseFraction(String v, int multiplier) {
    int r = 0;
    for (int i = 0; i < v.length(); i++) {
      char c = v.charAt(i);
      int x = c < '0' || c > '9' ? 0 : (c - '0');
      r += multiplier * x;
      if (multiplier < 10) {
        // We're at the last digit. Check for rounding.
        if (i + 1 < v.length()
            && v.charAt(i + 1) >= '5') {
          ++r;
        }
        break;
      }
      multiplier /= 10;
    }
    return r;
  }

  /** Check that the combination year, month, date forms a legal date. */
  static void checkLegalDate(int year, int month, int day, String full) {
    if (day > daysInMonth(year, month)) {
      throw fieldOutOfRange("DAY", full);
    }
    if (month < 1 || month > 12) {
      throw fieldOutOfRange("MONTH", full);
    }
    if (year <= 0) {
      // Year 0 is not really a legal value.
      throw fieldOutOfRange("YEAR", full);
    }
  }

  /** Lenient date validation.  This accepts more date strings
   * than validateDate: it does not insist on having two-digit
   * values for days and months, and accepts spaces around the value.
   * @param s     A string representing a date.
   */
  private static void validateLenientDate(String s) {
    Matcher matcher = LENIENT_DATE_PATTERN.matcher(s);
    if (matcher.find()) {
      int year = Integer.parseInt(matcher.group(1));
      int month = Integer.parseInt(matcher.group(2));
      int day = Integer.parseInt(matcher.group(3));
      checkLegalDate(year, month, day, s);
    } else {
      throw invalidType("DATE", s);
    }
  }

  private static void validateDate(String s, String full) {
    Matcher matcher = ISO_DATE_PATTERN.matcher(s);
    if (matcher.find()) {
      int year = Integer.parseInt(matcher.group(1));
      int month = Integer.parseInt(matcher.group(2));
      int day = Integer.parseInt(matcher.group(3));
      checkLegalDate(year, month, day, full);
    } else {
      throw invalidType("DATE", full);
    }
  }

  /** Returns the number of days in a month in the proleptic Gregorian calendar
   * used by ISO-8601.
   *
   * <p>"Proleptic" means that we apply the calendar to dates before the
   * Gregorian calendar was invented (in 1582). Thus, years 0 and 1200 are
   * considered leap years, and 1500 is not. */
  private static int daysInMonth(int year, int month) {
    switch (month) {
    case 9:
    case 4:
    case 6:
    case 11:
        // Thirty days hath September,
        // April, June, and November,
      return 30;

    default:
      // All the rest have thirty-one,
      return 31;

    case 2:
      // Except February, twenty-eight days clear,
      // And twenty-nine in each leap year.
      return isLeapYear(year) ? 29 : 28;
    }
  }

  /** Whether a year is considered a leap year in the proleptic Gregorian
   * calendar. */
  private static boolean isLeapYear(int year) {
    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
  }

  private static void validateTime(String time, String full) {
    Matcher matcher = ISO_TIME_PATTERN.matcher(time);
    if (matcher.find()) {
      int hour = Integer.parseInt(matcher.group(1));
      if (hour > 23) {
        throw fieldOutOfRange("HOUR", full);
      }
    } else {
      throw invalidType("TIME", full);
    }
  }

  private static IllegalArgumentException fieldOutOfRange(String field,
      String full) {
    return new IllegalArgumentException("Value of " + field
        + " field is out of range in '" + full + "'");
  }

  private static IllegalArgumentException invalidType(String type,
      String full) {
    return new IllegalArgumentException("Invalid " + type + " value, '"
        + full + "'");
  }

  public static long timestampStringToUnixDate(String s) {
    try {
      return timestampStringToUnixDate0(s);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException(e.getMessage());
    }
  }

  private static long timestampStringToUnixDate0(String s) {
    final long d;
    final long t;
    s = s.trim();
    int space = s.indexOf(' ');
    if (space >= 0) {
      String datePart = s.substring(0, space);
      validateDate(datePart, s);
      d = dateStringToUnixDate(datePart);

      String timePart = s.substring(space + 1);
      validateTime(timePart, s);
      t = timeStringToUnixDate(timePart);
    } else {
      validateDate(s, s);
      d = dateStringToUnixDate(s);
      t = 0;
    }
    return d * MILLIS_PER_DAY + t;
  }

  public static long unixDateExtract(TimeUnitRange range, long date) {
    switch (range) {
    case EPOCH:
      // no need to extract year/month/day, just multiply
      return date * SECONDS_PER_DAY;
    default:
      return julianExtract(range, (int) date + EPOCH_JULIAN);
    }
  }

  private static int julianExtract(TimeUnitRange range, int julian) {
    // this shifts the epoch back to astronomical year -4800 instead of the
    // start of the Christian era in year AD 1 of the proleptic Gregorian
    // calendar.
    int j = julian + 32044;
    int g = j / 146097;
    int dg = j % 146097;
    int c = (dg / 36524 + 1) * 3 / 4;
    int dc = dg - c * 36524;
    int b = dc / 1461;
    int db = dc % 1461;
    int a = (db / 365 + 1) * 3 / 4;
    int da = db - a * 365;

    // integer number of full years elapsed since March 1, 4801 BC
    int y = g * 400 + c * 100 + b * 4 + a;
    // integer number of full months elapsed since the last March 1
    int m = (da * 5 + 308) / 153 - 2;
    // number of days elapsed since day 1 of the month
    int d = da - (m + 4) * 153 / 5 + 122;
    int year = y - 4800 + (m + 2) / 12;
    int month = (m + 2) % 12 + 1;
    int day = d + 1;
    switch (range) {
    case YEAR:
      return year;
    case ISOYEAR:
      int weekNumber = getIso8601WeekNumber(julian, year, month, day);
      if (weekNumber == 1 && month == 12) {
        return year + 1;
      } else if (month == 1 && weekNumber > 50) {
        return year - 1;
      }
      return year;
    case QUARTER:
      return (month + 2) / 3;
    case MONTH:
      return month;
    case DAY:
      return day;
    case DOW:
      return Math.floorMod(julian + 1, 7) + 1; // sun=1, sat=7
    case ISODOW:
      return Math.floorMod(julian, 7) + 1; // mon=1, sun=7
    case WEEK:
      return getIso8601WeekNumber(julian, year, month, day);
    case DOY:
      final long janFirst = ymdToJulian(year, 1, 1);
      return (int) (julian - janFirst) + 1;
    case DECADE:
      return year / 10;
    case CENTURY:
      return year > 0
          ? (year + 99) / 100
          : (year - 99) / 100;
    case MILLENNIUM:
      return year > 0
          ? (year + 999) / 1000
          : (year - 999) / 1000;
    default:
      throw new AssertionError(range);
    }
  }

  /** Returns the first day of the first week of a year.
   * Per ISO-8601 it is the Monday of the week that contains Jan 4,
   * or equivalently, it is a Monday between Dec 29 and Jan 4.
   * Sometimes it is in the year before the given year. */
  private static long firstMondayOfFirstWeek(int year) {
    final long janFirst = ymdToJulian(year, 1, 1);
    final long janFirstDow = Math.floorMod(janFirst + 1, 7L); // sun=0, sat=6
    return janFirst + (11 - janFirstDow) % 7 - 3;
  }

  /** Returns the ISO-8601 week number based on year, month, day.
   * Per ISO-8601 it is the Monday of the week that contains Jan 4,
   * or equivalently, it is a Monday between Dec 29 and Jan 4.
   * Sometimes it is in the year before the given year, sometimes after. */
  private static int getIso8601WeekNumber(int julian, int year, int month, int day) {
    long fmofw = firstMondayOfFirstWeek(year);
    if (month == 12 && day > 28) {
      if (31 - day + 4 > 7 - (Math.floorMod(julian, 7) + 1)
          && 31 - day + Math.floorMod(julian, 7) + 1 >= 4) {
        return (int) (julian - fmofw) / 7 + 1;
      } else {
        return 1;
      }
    } else if (month == 1 && day < 5) {
      if (4 - day <= 7 - (Math.floorMod(julian, 7) + 1)
          && day - (Math.floorMod(julian, 7) + 1) >= -3) {
        return 1;
      } else {
        return (int) (julian - firstMondayOfFirstWeek(year - 1)) / 7 + 1;
      }
    }
    return (int) (julian - fmofw) / 7 + 1;
  }

  /** Extracts a time unit from a UNIX date (milliseconds since epoch). */
  public static int unixTimestampExtract(TimeUnitRange range,
      long timestamp) {
    return unixTimeExtract(range,
        (int) Math.floorMod(timestamp, MILLIS_PER_DAY));
  }

  /** Extracts a time unit from a time value (milliseconds since midnight). */
  public static int unixTimeExtract(TimeUnitRange range, int time) {
    assert time >= 0;
    assert time < MILLIS_PER_DAY;
    switch (range) {
    case HOUR:
      return time / (int) MILLIS_PER_HOUR;
    case MINUTE:
      final int minutes = time / (int) MILLIS_PER_MINUTE;
      return minutes % 60;
    case SECOND:
      final int seconds = time / (int) MILLIS_PER_SECOND;
      return seconds % 60;
    default:
      throw new AssertionError(range);
    }
  }

  /** Resets to zero the "time" part of a timestamp. */
  public static long resetTime(long timestamp) {
    int date = (int) (timestamp / MILLIS_PER_DAY);
    return (long) date * MILLIS_PER_DAY;
  }

  /** Resets to epoch (1970-01-01) the "date" part of a timestamp. */
  public static long resetDate(long timestamp) {
    return Math.floorMod(timestamp, MILLIS_PER_DAY);
  }

  public static long unixTimestampFloor(TimeUnitRange range, long timestamp) {
    int date = (int) (timestamp / MILLIS_PER_DAY);
    final int f = julianDateFloor(range, date + EPOCH_JULIAN, true);
    return (long) f * MILLIS_PER_DAY;
  }

  public static long unixDateFloor(TimeUnitRange range, long date) {
    return julianDateFloor(range, (int) date + EPOCH_JULIAN, true);
  }

  public static long unixTimestampCeil(TimeUnitRange range, long timestamp) {
    int date = (int) (timestamp / MILLIS_PER_DAY);
    final int f = julianDateFloor(range, date + EPOCH_JULIAN, false);
    return (long) f * MILLIS_PER_DAY;
  }

  public static long unixDateCeil(TimeUnitRange range, long date) {
    return julianDateFloor(range, (int) date + EPOCH_JULIAN, false);
  }

  private static int julianDateFloor(TimeUnitRange range, int julian,
      boolean floor) {
    // this shifts the epoch back to astronomical year -4800 instead of the
    // start of the Christian era in year AD 1 of the proleptic Gregorian
    // calendar.
    int j = julian + 32044;
    int g = j / 146097;
    int dg = j % 146097;
    int c = (dg / 36524 + 1) * 3 / 4;
    int dc = dg - c * 36524;
    int b = dc / 1461;
    int db = dc % 1461;
    int a = (db / 365 + 1) * 3 / 4;
    int da = db - a * 365;

    // integer number of full years elapsed since March 1, 4801 BC
    int y = g * 400 + c * 100 + b * 4 + a;
    // integer number of full months elapsed since the last March 1
    int m = (da * 5 + 308) / 153 - 2;
    // number of days elapsed since day 1 of the month
    int d = da - (m + 4) * 153 / 5 + 122;
    int year = y - 4800 + (m + 2) / 12;
    int month = (m + 2) % 12 + 1;
    int day = d + 1;
    switch (range) {
    case MILLENNIUM:
      return floor
          ? ymdToUnixDate(1000 * ((year + 999) / 1000) - 999, 1, 1)
          : ymdToUnixDate(1000 * ((year + 999) / 1000) + 1, 1, 1);
    case CENTURY:
      return floor
          ? ymdToUnixDate(100 * ((year + 99) / 100) - 99, 1, 1)
          : ymdToUnixDate(100 * ((year + 99) / 100) + 1, 1, 1);
    case DECADE:
      return floor
          ? ymdToUnixDate(10 * (year / 10), 1, 1)
          : ymdToUnixDate(10 * (1 + year / 10), 1, 1);
    case YEAR:
      if (!floor && (month > 1 || day > 1)) {
        ++year;
      }
      return ymdToUnixDate(year, 1, 1);
    case ISOYEAR:
      final int isoWeek = getIso8601WeekNumber(julian, year, month, day);
      final int dowMon = Math.floorMod(julian, 7); // mon=0, sun=6
      final int isoYearFloor = julian - 7 * (isoWeek - 1) - dowMon;
      if (floor || isoYearFloor == julian) {
        return isoYearFloor - EPOCH_JULIAN;
      } else {
        // CEIL of this date is the FLOOR of the date 53 weeks later.
        // (Usually 52 weeks later, sometimes 53 weeks later.)
        return julianDateFloor(range, isoYearFloor + 7 * 53, true);
      }
    case QUARTER:
      final int q = (month - 1) / 3;
      if (!floor) {
        if (month - 1 > q * 3 || day > 1) {
          if (q == 3) {
            ++year;
            month = 1;
          } else {
            month = q * 3 + 4;
          }
        }
      } else {
        month = q * 3 + 1;
      }
      return ymdToUnixDate(year, month, 1);
    case MONTH:
      if (!floor && day > 1) {
        ++month;
      }
      return ymdToUnixDate(year, month, 1);
    case WEEK:
      final int dow = Math.floorMod(julian + 1, 7); // sun=0, sat=6
      int offset = dow;
      if (!floor && offset > 0) {
        offset -= 7;
      }
      return ymdToUnixDate(year, month, day) - offset;
    case DAY:
      return ymdToUnixDate(year, month, day);
    default:
      throw new AssertionError(range);
    }
  }

  public static int ymdToUnixDate(int year, int month, int day) {
    final int julian = ymdToJulian(year, month, day);
    return julian - EPOCH_JULIAN;
  }

  /** Calculates the Julian Day Number for any valid date in the Gregorian
   * calendar.
   *
   * <p>If date is invalid, result is unspecified.
   *
   * <p>See an
   * <a href="http://www.cs.utsa.edu/~cs1063/projects/Spring2011/Project1/jdn-explanation.html">
   * explanation</a> of this algorithm.
   *
   * @param year Year (e.g. 2020 means 2020 CE, 1 means 1 CE, 0 means 1 BCE
   *   because there is no 0 CE, -1 means 2 BCE, etc.)
   * @param month Month (between 1 and 12 inclusive, 1 meaning January)
   * @param day Day of month (between 1 and 31 inclusive) */
  public static int ymdToJulian(int year, int month, int day) {
    int a = (14 - month) / 12;
    int y = year + 4800 - a;
    int m = month + 12 * a - 3;
    return day + (153 * m + 2) / 5
        + 365 * y
        + y / 4
        - y / 100
        + y / 400
        - 32045;
  }

  public static long unixTimestamp(int year, int month, int day, int hour,
      int minute, int second) {
    final int date = ymdToUnixDate(year, month, day);
    return (long) date * MILLIS_PER_DAY
        + (long) hour * MILLIS_PER_HOUR
        + (long) minute * MILLIS_PER_MINUTE
        + (long) second * MILLIS_PER_SECOND;
  }

  /** Adds a given number of months to a timestamp, represented as the number
   * of milliseconds since the epoch. */
  public static long addMonths(long timestamp, int m) {
    final long millis =
        Math.floorMod(timestamp, DateTimeUtils.MILLIS_PER_DAY);
    timestamp -= millis;
    final long x =
        addMonths((int) (timestamp / DateTimeUtils.MILLIS_PER_DAY), m);
    return x * DateTimeUtils.MILLIS_PER_DAY + millis;
  }

  /** Adds a given number of months to a date, represented as the number of
   * days since the epoch. */
  public static int addMonths(int date, int m) {
    int y0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date);
    int m0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date);
    int d0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.DAY, date);
    m0 += m;
    int deltaYear = Math.floorDiv(m0, 12);
    y0 += deltaYear;
    m0 = Math.floorMod(m0, 12);
    if (m0 == 0) {
      y0 -= 1;
      m0 += 12;
    }

    int last = lastDay(y0, m0);
    if (d0 > last) {
      d0 = last;
    }
    return DateTimeUtils.ymdToUnixDate(y0, m0, d0);
  }

  /**
   * SQL {@code LAST_DAY} function.
   *
   * @param date days since epoch
   * @return days of the last day of the month since epoch
   */
  public static int lastDay(int date) {
    int y0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date);
    int m0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date);
    int last = lastDay(y0, m0);
    return DateTimeUtils.ymdToUnixDate(y0, m0, last);
  }

  private static int lastDay(int y, int m) {
    switch (m) {
    case 2:
      return y % 4 == 0
          && (y % 100 != 0
          || y % 400 == 0)
          ? 29 : 28;
    case 4:
    case 6:
    case 9:
    case 11:
      return 30;
    default:
      return 31;
    }
  }

  /** Finds the number of months between two dates, each represented as the
   * number of days since the epoch. */
  public static int subtractMonths(int date0, int date1) {
    if (date0 < date1) {
      return -subtractMonths(date1, date0);
    }

    int y0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date0);
    int m0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date0);
    int d0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.DAY, date0);

    int y1 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date1);
    int m1 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date1);
    int d1 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.DAY, date1);

    int years = y0 - y1;
    boolean adjust = m0 < m1 || m0 == m1 && d0 < d1;
    if (adjust) {
      years--;
    }

    int months = 12 * years;
    if (adjust) {
      months += 12 - (m1 - m0);
    } else {
      months += m0 - m1;
    }

    if (d0 < d1) {
      months--;
    }

    return months;
  }

  public static int subtractMonths(long t0, long t1) {
    final long millis0 = Math.floorMod(t0, DateTimeUtils.MILLIS_PER_DAY);
    final int d0 =
        (int) Math.floorDiv(t0 - millis0, DateTimeUtils.MILLIS_PER_DAY);
    final long millis1 = Math.floorMod(t1, DateTimeUtils.MILLIS_PER_DAY);
    final int d1 =
        (int) Math.floorDiv(t1 - millis1, DateTimeUtils.MILLIS_PER_DAY);
    int x = subtractMonths(d0, d1);
    final long d2 = addMonths(d1, x);
    if (d2 == d0 && millis0 < millis1) {
      --x;
    }
    return x;
  }

  /** Divide, rounding towards negative infinity.
   *
   * @deprecated Use {@link Math#floorDiv(long, long)} */
  @Deprecated // to be removed before 2.0
  public static long floorDiv(long x, long y) {
    long r = x / y;
    // if the signs are different and modulo not zero, round down
    if ((x ^ y) < 0 && r * y != x) {
      r--;
    }
    return r;
  }

  /** Modulo, always returning a non-negative result.
   *
   * @deprecated Use {@link Math#floorMod(long, long)} */
  @Deprecated // to be removed before 2.0
  public static long floorMod(long x, long y) {
    return x - floorDiv(x, y) * y;
  }

  /** Creates an instance of {@link Calendar} in the root locale and UTC time
   * zone. */
  public static Calendar calendar() {
    return Calendar.getInstance(UTC_ZONE, Locale.ROOT);
  }

  /** Returns whether a value is an {@code OffsetDateTime}. */
  public static boolean isOffsetDateTime(Object o) {
    return OFFSET_DATE_TIME_HANDLER.isOffsetDateTime(o);
  }

  /** Returns the value of a {@code OffsetDateTime} as a string. */
  public static String offsetDateTimeValue(Object o) {
    return OFFSET_DATE_TIME_HANDLER.stringValue(o);
  }

  /**
   * Calculates the unix date as the number of days since January 1st, 1970 UTC for the given SQL
   * date.
   *
   * @see #sqlDateToUnixDate(java.sql.Date, TimeZone)
   */
  public static int sqlDateToUnixDate(java.sql.Date date, Calendar calendar) {
    return sqlDateToUnixDate(date, calendar != null ? calendar.getTimeZone() : null);
  }

  /**
   * Calculates the unix date as the number of days since January 1st, 1970 UTC for the given SQL
   * date.
   *
   * <p>The {@link java.sql.Date} class uses the standard Gregorian calendar which switches from
   * the Julian calendar to the Gregorian calendar in October 1582. For compatibility with
   * ISO-8601, the value is converted to a {@link LocalDate} which uses the proleptic Gregorian
   * calendar.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the date
   * unmodified.
   *
   * <p>If the date contains a partial day, it will be rounded to a full day depending on the
   * milliseconds value. If the milliseconds value is positive, it will be rounded down to the
   * closest full day. If the milliseconds value is negative, it will be rounded up to the closest
   * full day.
   */
  public static int sqlDateToUnixDate(java.sql.Date date, TimeZone timeZone) {
    final long time = date.getTime();

    // Convert from standard Gregorian calendar to ISO calendar system
    // Use a SQL timestamp to include the time offset from UTC in the unix timestamp
    final LocalDateTime dateTime = new Timestamp(time).toLocalDateTime();
    long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
        * DateTimeUtils.MILLIS_PER_SECOND
        + dateTime.get(ChronoField.MILLI_OF_SECOND);

    // Calculate timezone offset in relation to local time
    if (timeZone != null) {
      unixTimestamp += timeZone.getOffset(time);
    }
    unixTimestamp -= DEFAULT_ZONE.getOffset(time);

    return (int) (unixTimestamp / DateTimeUtils.MILLIS_PER_DAY);
  }

  /**
   * Converts the given unix date to a SQL date.
   *
   * <p>The unix date should be the number of days since January 1st, 1970 UTC using the proleptic
   * Gregorian calendar as defined by ISO-8601. The returned {@link java.sql.Date} object will use
   * the standard Gregorian calendar which switches from the Julian calendar to the Gregorian
   * calendar in October 1582. This conversion is handled by the {@link java.sql.Date} class when
   * converting from a {@link LocalDate} object.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the date
   * unmodified.
   */
  public static java.sql.Date unixDateToSqlDate(int date, Calendar calendar) {
    // Convert unix date from the ISO calendar system to the standard Gregorian calendar
    final LocalDate localDate = LocalDate.ofEpochDay(date);
    final java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);

    // Calculate timezone offset in relation to local time
    final long time = sqlDate.getTime();
    final int offset = calendar != null ? calendar.getTimeZone().getOffset(time) : 0;
    sqlDate.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);

    return sqlDate;
  }

  /**
   * Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
   * given date.
   *
   * @see #utilDateToUnixTimestamp(Date, TimeZone)
   */
  public static long utilDateToUnixTimestamp(Date date, Calendar calendar) {
    return utilDateToUnixTimestamp(date, calendar != null ? calendar.getTimeZone() : null);
  }

  /**
   * Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
   * given date.
   *
   * <p>The {@link Date} class uses the standard Gregorian calendar which switches from the Julian
   * calendar to the Gregorian calendar in October 1582. For compatibility with ISO-8601, the value
   * is converted to a {@code java.time} object which uses the proleptic Gregorian calendar.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the date
   * unmodified.
   */
  public static long utilDateToUnixTimestamp(Date date, TimeZone timeZone) {
    final Timestamp timestamp = new Timestamp(date.getTime());
    return sqlTimestampToUnixTimestamp(timestamp, timeZone);
  }

  /**
   * Converts the given unix timestamp to a Java date.
   *
   * <p>The unix timestamp should be the number of milliseconds since January 1st, 1970 UTC using
   * the proleptic Gregorian calendar as defined by ISO-8601. The returned {@link Date} object will
   * use the standard Gregorian calendar which switches from the Julian calendar to the Gregorian
   * calendar in October 1582. This conversion is handled by the {@code java.sql} package when
   * converting from a {@code java.time} object.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the date
   * unmodified.
   */
  public static Date unixTimestampToUtilDate(long timestamp, Calendar calendar) {
    final Timestamp sqlTimestamp = unixTimestampToSqlTimestamp(timestamp, calendar);
    return new Date(sqlTimestamp.getTime());
  }

  /**
   * Calculates the unix time as the number of milliseconds since the previous day in UTC for the
   * given SQL time.
   *
   * @see #sqlTimeToUnixTime(Time, TimeZone)
   */
  public static int sqlTimeToUnixTime(Time time, Calendar calendar) {
    return sqlTimeToUnixTime(time, calendar != null ? calendar.getTimeZone() : null);
  }

  /**
   * Calculates the unix time as the number of milliseconds since the previous day in UTC for the
   * given SQL time.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the time
   * unmodified.
   */
  public static int sqlTimeToUnixTime(Time time, TimeZone timeZone) {
    long unixTime = time.getTime();
    if (timeZone != null) {
      unixTime += timeZone.getOffset(unixTime);
    }
    return (int) Math.floorMod(unixTime, MILLIS_PER_DAY);
  }

  /**
   * Converts the given unix time to a SQL time.
   *
   * <p>The unix time should be the number of milliseconds since the previous day in UTC.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the time
   * unmodified.
   */
  public static Time unixTimeToSqlTime(int time, Calendar calendar) {
    if (calendar != null) {
      time -= calendar.getTimeZone().getOffset(time);
    }
    return new Time(time);
  }

  /**
   * Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
   * given SQL timestamp.
   *
   * @see #sqlTimestampToUnixTimestamp(Timestamp, TimeZone)
   */
  public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, Calendar calendar) {
    return sqlTimestampToUnixTimestamp(timestamp, calendar != null ? calendar.getTimeZone() : null);
  }

  /**
   * Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
   * given SQL timestamp.
   *
   * <p>The {@link Timestamp} class uses the standard Gregorian calendar which switches from the
   * Julian calendar to the Gregorian calendar in October 1582. For compatibility with ISO-8601,
   * the value is converted to a {@link LocalDateTime} which uses the proleptic Gregorian calendar.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the
   * timestamp unmodified.
   */
  public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, TimeZone timeZone) {
    final long time = timestamp.getTime();

    // Convert SQL timestamp from standard Gregorian calendar to ISO calendar system
    final LocalDateTime dateTime = timestamp.toLocalDateTime();
    long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
        * DateTimeUtils.MILLIS_PER_SECOND
        + dateTime.get(ChronoField.MILLI_OF_SECOND);

    // Calculate timezone offset in relation to local time
    if (timeZone != null) {
      unixTimestamp += timeZone.getOffset(time);
    }
    unixTimestamp -= DEFAULT_ZONE.getOffset(time);

    return unixTimestamp;
  }

  /**
   * Converts the given unix timestamp to a SQL timestamp.
   *
   * <p>The unix timestamp should be the number of milliseconds since January 1st, 1970 UTC using
   * the proleptic Gregorian calendar as defined by ISO-8601. The returned {@link Timestamp} object
   * will use the standard Gregorian calendar which switches from the Julian calendar to the
   * Gregorian calendar in October 1582. This conversion is handled by the {@link Timestamp} class
   * when converting from a {@link LocalDateTime} object.
   *
   * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
   * timezone instead of UTC. Providing the default timezone or {@code null} will return the
   * timestamp unmodified.
   */
  public static Timestamp unixTimestampToSqlTimestamp(long timestamp, Calendar calendar) {
    // Convert unix timestamp from the ISO calendar system to the standard Gregorian calendar
    final LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(
        Math.floorDiv(timestamp, DateTimeUtils.MILLIS_PER_SECOND),
        (int) (Math.floorMod(timestamp, DateTimeUtils.MILLIS_PER_SECOND) * NANOS_PER_MILLI),
        ZoneOffset.UTC);
    final Timestamp sqlTimestamp = Timestamp.valueOf(localDateTime);

    // Calculate timezone offset in relation to local time
    final long time = sqlTimestamp.getTime();
    final int offset = calendar != null ? calendar.getTimeZone().getOffset(time) : 0;
    sqlTimestamp.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);

    return sqlTimestamp;
  }

  //~ Inner Classes ----------------------------------------------------------

  /**
   * Helper class for {@link DateTimeUtils#parsePrecisionDateTimeLiteral}
   */
  public static class PrecisionTime {
    private final Calendar cal;
    private final String fraction;
    private final int precision;

    public PrecisionTime(Calendar cal, String fraction, int precision) {
      this.cal = cal;
      this.fraction = fraction;
      this.precision = precision;
    }

    public Calendar getCalendar() {
      return cal;
    }

    public int getPrecision() {
      return precision;
    }

    public String getFraction() {
      return fraction;
    }
  }

  /** Deals with values of {@code java.time.OffsetDateTime} without introducing
   * a compile-time dependency (because {@code OffsetDateTime} is only JDK 8 and
   * higher). */
  private interface OffsetDateTimeHandler {
    boolean isOffsetDateTime(Object o);
    String stringValue(Object o);
  }

  /** Implementation of {@code OffsetDateTimeHandler} for environments where
   * no instances are possible. */
  private static class NoopOffsetDateTimeHandler
      implements OffsetDateTimeHandler {
    public boolean isOffsetDateTime(Object o) {
      return false;
    }

    public String stringValue(Object o) {
      throw new UnsupportedOperationException();
    }
  }

  /** Implementation of {@code OffsetDateTimeHandler} for environments where
   * no instances are possible. */
  private static class ReflectiveOffsetDateTimeHandler
      implements OffsetDateTimeHandler {
    final Class offsetDateTimeClass;

    private ReflectiveOffsetDateTimeHandler() throws ClassNotFoundException {
      offsetDateTimeClass = Class.forName("java.time.OffsetDateTime");
    }

    public boolean isOffsetDateTime(Object o) {
      return o != null && o.getClass() == offsetDateTimeClass;
    }

    public String stringValue(Object o) {
      return o.toString();
    }
  }
}

// End DateTimeUtils.java
